/*************************************************
func: 封装socket 基础接口
author: zyh
date: 2024.4
**************************************************/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>   
#include <errno.h>
#include <arpa/inet.h>//提供IP地址转换函数
#include <sys/types.h>//数据类型定义文件
#include <sys/socket.h>//提供socket函数及数结构
#include <netinet/in.h>//定义数据结构体sockaddr_in
#include <netinet/ip.h>
#include <pthread.h>
#include "socket_api.h"
 
/*
非阻塞模式下循环读取直到缓冲区没有数据可读为止
非阻塞模式下如果没有数据会返回，不会阻塞着读，因此需要循环读取直到缓冲区没有数据可读为止
返回：-1 出错，0  连接关闭，成功返回实际接收到字节数
*/
ssize_t socket_recv_again(int fd, void *buf, size_t count)
{
	ssize_t n = 0;//累计字节数
    size_t nleft = 0;//剩余的字节数
    ssize_t nread = 0;//当前接收的字节数
    
	char *ptr = (char *)buf;
 
	nleft = count;
    while (0 < nleft) {//由于不能保证一次读操作能够返回字节数是多少，所以需要进行循环来接收
        //printf("0000000\n");
    	nread = recv(fd, ptr + n, nleft, 0);// <0：出错，=0：连接关闭，>0接收到数据大小
        if (0 > nread) {
            if (errno == EAGAIN) {//EAGAIN:表示当前没有数据可读取了
                //printf("111111\n");
				break;
			} else if (errno == EINTR) {// 被信号中断，重试
                continue;
            } else { // 其他错误，退出读取
				return -1;
			}
        } else if (0 == nread) {//对方关闭了
            return 0;
        }
 
        n += nread;
        nleft -= nread;
    } 
 
    return n;
}
 
//确保写入n个字节，返回：-1 出错，0  连接关闭，成功返回实际字节数
ssize_t socket_sendn(int fd, const void *buf, size_t n)
{
	size_t nleft = 0; // 剩余未写的字节数
	ssize_t nwritten = 0; // 单次写入的字节数
 
	char *ptr = (char *)buf;
	nleft = n;
    while (nleft > 0) {
		nwritten = send(fd, ptr, nleft, 0);
		if (0 > nwritten) {
			 if (errno == EINTR) { // 写入操作被信号中断，继续写入
				 continue;
			 } else { //其他错误，退出
                 return -1;
			 }
		} else if (0 == nwritten) {//对方关闭了
            return 0;
        }
        nleft -= nwritten;
        ptr += nwritten;
    }
 
    //return (n - nleft); /* return >= 0 */
    return n;
}
 
 
#if 0
/*非阻塞模式下，确保读取n个字节*/
static ssize_t readn(int fd, void *buf, size_t n)
{
    size_t nleft = 0; // 剩余未读的字节数
    ssize_t nread = 0; // 单次读到的字节数
    char *ptr = NULL; // 指向当前读写位置的指针
 
    ptr = buf;
    nleft = n;
    while (nleft > 0) {
		nread = read(fd, ptr, nleft);
        if (nread < 0) {
            if (errno == EINTR) {// 被信号中断，重试
				continue;
            } else { // 其他错误
                return -1;
            }
        } else if (nread == 0) {
            // 已达到文件结尾
            break;
        }
 
        nleft -= nread;
        ptr += nread;
    }
    return (n - nleft); // 返回实际读取的字节数
}
#endif
 
 
//监听文件描述符是否可读，返回0可读，返回-1 监听超时(不可读)或失败
int select_fd(int fd, int timeout_ms)
{
	struct timeval time_out;
	time_out.tv_sec = timeout_ms/1000;
	time_out.tv_usec = (timeout_ms - time_out.tv_sec*1000)*1000;
 	
	fd_set readfd; 
	FD_ZERO(&readfd);
	FD_SET(fd, &readfd);
	if (0 >= select(fd + 1, &readfd, NULL, NULL, &time_out)) {//超时
        return -1;
	}
	if (0 == FD_ISSET(fd, &readfd)) {
		return -1;	
	}
 
    return 0;
}
 
 
 
//将文件描述符默认的阻塞行为修改为非阻塞
int set_fd_nonblock(int fd)
{
	int flag = fcntl(fd, F_GETFL);//设置为非阻塞
	if (0 > flag) {
		return -1;
	}
	if (0 > fcntl(fd, F_SETFL, flag | O_NONBLOCK)) {
		return -1;
	}
	return 0;
}
 
 
/**
功能：创建并初始化socket
入参：
    socket_type：指明通信字节流类型，取值如：
            SOCK_STREAM：流式套接字（TCP方式）
            SOCK_DGRAM：数据包套接字（UDP方式）
            SOCK_RAM：原始套接字
    socket_protocol:  表示传输协议，常用的有 IPPROTO_TCP 和IPPTOTO_UDP，分别表示 TCP 传输协议和 UDP 传输协议。通常设置为0 （IPPROTO_TCP）
    broadcast_flag：发送UDP数据时是否广播
出参：无
返回：成功返回socket描述符，失败-1
**/
int socket_init(int socket_type, int socket_protocol, int broadcast_flag)
{
	int sockfd = 0;
    
	sockfd = socket(AF_INET, socket_type, socket_protocol);
	if (0 > sockfd) {
		perror("socket");
		return -1;
	}
    
    int reuse = 1;//允许地址的快速重用
	if (0 > setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(int))) {
		perror("set sockopt");
		close(sockfd);
		return -1;
	}
 
#if 0//读缓冲区大小
	unsigned long snd_size = 0;/*发送缓冲区大小*/
	unsigned long rcv_size = 0;/*选项值长度*/
	socklen_t optlen;/*选项值长度*/ 
	optlen = sizeof(snd_size);//获得原始发送缓冲区大小
	getsockopt(sockfd, SOL_SOCKET, SO_SNDBUF, &snd_size, &optlen);
	optlen = sizeof(rcv_size);
	getsockopt(sockfd, SOL_SOCKET, SO_RCVBUF, &rcv_size, &optlen);
	printf("发送缓冲区原始大小为:%ld \n", snd_size);
	printf("接收缓冲区原始大小为:%ld \n", rcv_size);
#endif
	
#if 0//设置缓冲区大小
	// 接收缓冲区
	int nRecvBuf = 48*1024;//设置为48K
	if(0 > setsockopt(sockfd, SOL_SOCKET, SO_RCVBUF, (const char*)&nRecvBuf, sizeof(int))) {
		perror("setsockopt SO_RCVBUF");
		close(sockfd);
		return -1;
	}
	
	//发送缓冲区
	int nSendBuf = 48*1024;//设置为48K
	if(0 > setsockopt(sockfd, SOL_SOCKET, SO_SNDBUF, (const char*)&nSendBuf, sizeof(int))) {
		perror("setsockopt SO_SNDBUF");
		close(sockfd);
		return -1;
	}
#endif
 
    if (broadcast_flag) {
        //一般在发送UDP数据报的时候，希望该socket发送的数据具有广播特性
        int bBroadcast = 1;
        if (0 > setsockopt(sockfd, SOL_SOCKET, SO_BROADCAST, &bBroadcast, sizeof(int))) {
            perror("setsockopt SO_BROADCAST");
            close(sockfd);
            return -1;
        }
    }
 
	return sockfd;
}
 
//客户端连接服务端，返回：成功：0， 失败：-1
int socket_client_connect(int sockfd, char *server_ip, int server_port)
{
    struct sockaddr_in serv_addr;
    memset(&serv_addr, 0, sizeof(serv_addr));
 
    serv_addr.sin_family = AF_INET;
    serv_addr.sin_port = htons(server_port);
 
    if (0 >= inet_pton(AF_INET, server_ip, &serv_addr.sin_addr)) {
        perror("inet_pton error");
        return -1;
    }
	
	if (0 > connect(sockfd, (struct sockaddr *)&serv_addr, sizeof(serv_addr))) {
		perror("connect");
		return -1;
	}
	
	return 0;
}
 
/**
功能：socket客户端初始化
入参：
    socket_type：指明通信字节流类型，取值如：
            SOCK_STREAM：流式套接字（TCP方式）
            SOCK_DGRAM：数据包套接字（UDP方式）
            SOCK_RAM：原始套接字
    socket_protocol:  表示传输协议，常用的有 IPPROTO_TCP 和IPPROTO_UDP，分别表示 TCP 传输协议和 UDP 传输协议。通常设置为0 （IPPROTO_TCP）
    broadcast_flag：发送UDP数据时是否广播
    bind_port：大于0则绑定本地端口
出参：无
返回：成功返回socket描述符，失败-1
**/
int socket_client_init(int socket_type, int socket_protocol, int broadcast_flag, int bind_port)
{
    int socket_fd = 0;
    
    socket_fd = socket_init(socket_type, socket_protocol, broadcast_flag);
    if (0 > socket_fd) {
        printf("socket_init failed\n");
        return -1;
    }
    
    if (0 < bind_port) {//客户端是否绑定本地端口
        /*绑定端口*/
        struct sockaddr_in client_addr;
        memset(&client_addr, 0, sizeof(client_addr));
        client_addr.sin_family = AF_INET;
        client_addr.sin_port = htons(bind_port);
        client_addr.sin_addr.s_addr = INADDR_ANY;
        
        if (0 > bind(socket_fd, (struct sockaddr *)&client_addr, sizeof(client_addr))) {
            //perror("client_addr bind failed");
            close(socket_fd);
            return -1;
        }
    }
    
	return socket_fd;
}
 
/**
功能：socket服务端初始化
入参：
    socket_type：指明通信字节流类型，取值如：
            SOCK_STREAM：流式套接字（TCP方式）
            SOCK_DGRAM：数据包套接字（UDP方式）
            SOCK_RAM：原始套接字
    socket_protocol:  表示传输协议，常用的有 IPPROTO_TCP 和IPPROTO_UDP，分别表示 TCP 传输协议和 UDP 传输协议。通常设置为0 （IPPROTO_TCP）
    broadcast_flag：发送UDP数据时是否广播
    ip：本地ip地址（“127.0.0.1”为内部访问 或 “0.0.0.0”为内外部都可访问）
    bind_port：作为服务器时绑定的本地端口
    max_listen_backlog：socket监听客户端连接套接口排队的最大连接个数
出参：无
返回：成功返回socket描述符，失败-1
**/
int socket_server_init(int socket_type, int socket_protocol, int broadcast_flag, const char *ip, int bind_port, int max_listen_backlog)
{
    int socket_fd = 0;
    socket_fd = socket_init(socket_type, socket_protocol, broadcast_flag);
    if (0 > socket_fd) {
        printf("socket_init failed\n");
        return -1;
    }
 
	/*绑定地址和端口*/
	struct sockaddr_in server_addr;
    memset(&server_addr, 0, sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(bind_port);
	if (strstr(ip, "127.0.0.1")) {//服务端程序只能在本机上测试，无法接受来自其他机器的连接请求
		server_addr.sin_addr.s_addr = inet_addr("127.0.0.1");
	} else {//服务端程序可以接受任何机器的连接请求，即绑定到此机器的所有网卡上
		server_addr.sin_addr.s_addr = INADDR_ANY;
	}
	
	if (0 > bind(socket_fd, (struct sockaddr *)&server_addr, sizeof(server_addr))) {
		perror("bind");
		close(socket_fd);
		return -1;
    }
	
	/*监听*/
    if (0 > listen(socket_fd, max_listen_backlog)) {
        perror("listen error");
		close(socket_fd);
		return -1;
    }
	
	return socket_fd;
}
 
 
/**
函数功能：socket非堵塞接收消息（select实现，监听最大文件描述符数量为1024， 如果存在大量客户端连接的情况下，文件描述符过多，则不适用）
入参：fd：串口文件描述符
	  buf：接收数据的缓冲区
	  len：缓冲区的大小
	  timeout_ms：超时时间，毫秒
出参：无
返回：成功返回实际接收的字节数，0已断开连接， -1失败，-2超时无数据可读
**/  
int socket_rcv_nonblock(int socket_fd, void *buf, int size, int timeout_ms)
{
    if (0 > select_fd(socket_fd, timeout_ms)) {//超时或不可读写
        return -2;  
    }
    
	int len = 0;
    
#if 0
    /*
    当 recv 函数返回 -1 时，可以检查 errno 来确定错误的原因
    EAGAIN：没有数据可读
    EINTR：接收操作被中断
    上面采用的select监听，因此不用判断EAGAIN的情况
     */
	len = recv(socket_fd, buf, size, 0);
#else
    len = socket_recv_again(socket_fd, buf, size);
#endif
    if (0 == len) {//关闭了连接
        return 0;
    } else if (0 > len) {
        return -1;
    }
 
	return len;
}